<!--
@license
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<link rel="import" href="../lib/case-map.html">
<link rel="import" href="../lib/path.html">
<script>

  /**
   * Changes to an object sub-field (aka "path") via a binding
   * (e.g. `<x-foo value="{{item.subfield}}"`) will notify other elements bound to
   * the same object automatically.
   *
   * When modifying a sub-field of an object imperatively
   * (e.g. `this.item.subfield = 42`), in order to have the new value propagated
   * to other elements, a special `set(path, value)` API is provided.
   * `set` sets the object field at the path specified, and then notifies the
   * binding system so that other elements bound to the same path will update.
   *
   * Example:
   *
   *     Polymer({
   *
   *       is: 'x-date',
   *
   *       properties: {
   *         date: {
   *           type: Object,
   *           notify: true
   *          }
   *       },
   *
   *       attached: function() {
   *         this.date = {};
   *         setInterval(function() {
   *           var d = new Date();
   *           // Required to notify elements bound to date of changes to sub-fields
   *           // this.date.seconds = d.getSeconds(); <-- Will not notify
   *           this.set('date.seconds', d.getSeconds());
   *           this.set('date.minutes', d.getMinutes());
   *           this.set('date.hours', d.getHours() % 12);
   *         }.bind(this), 1000);
   *       }
   *
   *     });
   *
   *  Allows bindings to `date` sub-fields to update on changes:
   *
   *     <x-date date="{{date}}"></x-date>
   *
   *     Hour: <span>{{date.hours}}</span>
   *     Min:  <span>{{date.minutes}}</span>
   *     Sec:  <span>{{date.seconds}}</span>
   *
   * @class data feature: path notification
   */

  (function() {
    // Using strict here to ensure fast argument manipulation in array methods
    'use strict';

    var Path = Polymer.Path;

    Polymer.Base._addFeature({
      /**
       * Notify that a path has changed.
       *
       * Example:
       *
       *     this.item.user.name = 'Bob';
       *     this.notifyPath('item.user.name');
       *
       * @param {string} path Path that should be notified.
      */
      notifyPath: function(path, value, fromAbove) {
        // Convert any array indices to keys before notifying path
        var info = {};
        var v = this._get(path, this, info);
        if (arguments.length === 1) {
          value = v;
        }
        // Notify change to key-based path
        if (info.path) {
          this._notifyPath(info.path, value, fromAbove);
        }
      },

      // Note: this implemetation only accepts key-based array paths
      _notifyPath: function(path, value, fromAbove) {
        var old = this._propertySetter(path, value);
        // manual dirty checking for now...
        // NaN is always not equal to itself,
        // if old and value are both NaN we treat them as equal
        // x === x is 10x faster, and equivalent to !isNaN(x)
        if (old !== value && (old === old || value === value)) {
          // console.group((this.localName || this.dataHost.id + '-' + this.dataHost.dataHost.index) + '#' + (this.id || this.index) + ' ' + path, value);
          // Take path effects at this level for exact path matches,
          // and notify down for any bindings to a subset of this path
          this._pathEffector(path, value);
          // Send event to notify the path change upwards
          // Optimization: don't notify up if we know the notification
          // is coming from above already (avoid wasted event dispatch)
          if (!fromAbove) {
            // TODO(sorvell): should only notify if notify: true?
            this._notifyPathUp(path, value);
          }
          // console.groupEnd((this.localName || this.dataHost.id + '-' + this.dataHost.dataHost.index) + '#' + (this.id || this.index) + ' ' + path, value);
          return true;
        }
      },

      /**
        Converts a path to an array of path parts.  A path may be specified
        as a dotted string or an array of one or more dotted strings (or numbers,
        for number-valued keys).
      */
      _getPathParts: function(path) {
        if (Array.isArray(path)) {
          var parts = [];
          for (var i=0; i<path.length; i++) {
            var args = path[i].toString().split('.');
            for (var j=0; j<args.length; j++) {
              parts.push(args[j]);
            }
          }
          return parts;
        } else {
          return path.toString().split('.');
        }
      },

      /**
       * Convienence method for setting a value to a path and notifying any
       * elements bound to the same path.
       *
       * Note, if any part in the path except for the last is undefined,
       * this method does nothing (this method does not throw when
       * dereferencing undefined paths).
       *
       * @method set
       * @param {(string|Array<(string|number)>)} path Path to the value
       *   to write.  The path may be specified as a string (e.g. `foo.bar.baz`)
       *   or an array of path parts (e.g. `['foo.bar', 'baz']`).  Note that
       *   bracketed expressions are not supported; string-based path parts
       *   *must* be separated by dots.  Note that when dereferencing array
       *   indices, the index may be used as a dotted part directly
       *   (e.g. `users.12.name` or `['users', 12, 'name']`).
       * @param {*} value Value to set at the specified path.
       * @param {Object=} root Root object from which the path is evaluated.
      */
      set: function(path, value, root) {
        var prop = root || this;
        var parts = this._getPathParts(path);
        var array;
        var last = parts[parts.length-1];
        if (parts.length > 1) {
          // Loop over path parts[0..n-2] and dereference
          for (var i=0; i<parts.length-1; i++) {
            var part = parts[i];
            if (array && part[0] == '#') {
              // Part was key; lookup item in collection
              prop = Polymer.Collection.get(array).getItem(part);
            } else {
              // Get item from simple property dereference
              prop = prop[part];
              if (array && (parseInt(part, 10) == part)) {
                // Translate array indices to collection keys for path notificaiton
                parts[i] = Polymer.Collection.get(array).getKey(prop);
              }
            }
            if (!prop) {
              return;
            }
            // Cache previous part if it is an array
            array = Array.isArray(prop) ? prop : null;
          }
          // Special handling when last part is a array item: need to replace
          // item in collection associated with key for that item
          if (array) {
            var coll = Polymer.Collection.get(array);
            var old, key;
            if (last[0] == '#') {
              // Part was key; lookup item in collection
              key = last;
              old = coll.getItem(key);
              // Update last part from key to index: O(n) lookup unavoidable
              last = array.indexOf(old);
              // Replace item associated with key in collection
              coll.setItem(key, value);
            } else if (parseInt(last, 10) == last) {
              // Dereference index & lookup collection key
              old = prop[last];
              key = coll.getKey(old);
              // Translate array indices to collection keys for path notificaiton
              parts[i] = key;
              // Replace item associated with key in collection
              coll.setItem(key, value);
            }
          }
          // Set value to object at end of path
          prop[last] = value;
          // Notify observers of path change
          if (!root) {
            this._notifyPath(parts.join('.'), value);
          }
        } else {
          // Simple property set
          prop[path] = value;
        }
      },

      /**
       * Convienence method for reading a value from a path.
       *
       * Note, if any part in the path is undefined, this method returns
       * `undefined` (this method does not throw when dereferencing undefined
       * paths).
       *
       * @method get
       * @param {(string|Array<(string|number)>)} path Path to the value
       *   to read.  The path may be specified as a string (e.g. `foo.bar.baz`)
       *   or an array of path parts (e.g. `['foo.bar', 'baz']`).  Note that
       *   bracketed expressions are not supported; string-based path parts
       *   *must* be separated by dots.  Note that when dereferencing array
       *   indices, the index may be used as a dotted part directly
       *   (e.g. `users.12.name` or `['users', 12, 'name']`).
       * @param {Object=} root Root object from which the path is evaluated.
       * @return {*} Value at the path, or `undefined` if any part of the path
       *   is undefined.
       */
      get: function(path, root) {
        return this._get(path, root);
      },

      // If `info` object is supplied, a `path` property will be added to it
      // containing the path with array indices converted to keys, for use
      // by the private _notifyPath / _notifySplice implementations
      _get: function(path, root, info) {
        var prop = root || this;
        var parts = this._getPathParts(path);
        var array;
        // Loop over path parts[0..n-1] and dereference
        for (var i=0; i<parts.length; i++) {
          if (!prop) {
            return;
          }
          var part = parts[i];
          if (array && part[0] == '#') {
            // Part was key; lookup item in collection
            prop = Polymer.Collection.get(array).getItem(part);
          } else {
            // Get item from simple property dereference
            prop = prop[part];
            if (info && array && (parseInt(part, 10) == part)) {
              // Translate array indices to collection keys for path notificaiton
              parts[i] = Polymer.Collection.get(array).getKey(prop);
            }
          }
          // Cache previous part if it is an array
          array = Array.isArray(prop) ? prop : null;
        }
        if (info) {
          info.path = parts.join('.');
        }
        return prop;
      },

      _pathEffector: function(path, value) {
        // get root property
        var model = Path.root(path);
        // search property effects of the root property for 'annotation' effects
        var fx$ = this._propertyEffects && this._propertyEffects[model];
        if (fx$) {
          for (var i=0, fx; (i<fx$.length) && (fx=fx$[i]); i++) {
            // use memoized path functions
            var fxFn = fx.pathFn;
            if (fxFn) {
              fxFn.call(this, path, value, fx.effect);
            }
          }
        }
        // notify runtime-bound paths
        if (this._boundPaths) {
          this._notifyBoundPaths(path, value);
        }
      },

      _annotationPathEffect: function(path, value, effect) {
        if (Path.matches(effect.value, false, path)) {
          // TODO(sorvell): ideally the effect function is on this prototype
          // so we don't have to call it like this.
          Polymer.Bind._annotationEffect.call(this, path, value, effect);
        } else if (!effect.negate && Path.isDescendant(effect.value, path)) {
          // locate the bound node
          var node = this._nodes[effect.index];
          if (node && node._notifyPath) {
            var newPath = Path.translate(effect.value, effect.name, path);
            node._notifyPath(newPath, value, true);
          }
        }
      },

      _complexObserverPathEffect: function(path, value, effect) {
        if (Path.matches(effect.trigger.name, effect.trigger.wildcard, path)) {
          Polymer.Bind._complexObserverEffect.call(this, path, value, effect);
        }
      },

      _computePathEffect: function(path, value, effect) {
        if (Path.matches(effect.trigger.name, effect.trigger.wildcard, path)) {
          Polymer.Bind._computeEffect.call(this, path, value, effect);
        }
      },

      _annotatedComputationPathEffect: function(path, value, effect) {
        if (Path.matches(effect.trigger.name, effect.trigger.wildcard, path)) {
          Polymer.Bind._annotatedComputationEffect.call(this, path, value, effect);
        }
      },

      /**
       * Aliases one data path as another, such that path notifications from one
       * are routed to the other.
       *
       * @method linkPaths
       * @param {string} to Target path to link.
       * @param {string} from Source path to link.
       */
      linkPaths: function(to, from) {
        this._boundPaths = this._boundPaths || {};
        if (from) {
          this._boundPaths[to] = from;
          // this.set(to, this._get(from));
        } else {
          this.unlinkPaths(to);
          // this.set(to, from);
        }
      },

      /**
       * Removes a data path alias previously established with `linkPaths`.
       *
       * Note, the path to unlink should be the target (`to`) used when
       * linking the paths.
       *
       * @method unlinkPaths
       * @param {string} path Target path to unlink.
       */
      unlinkPaths: function(path) {
        if (this._boundPaths) {
          delete this._boundPaths[path];
        }
      },

      _notifyBoundPaths: function(path, value) {
        for (var a in this._boundPaths) {
          var b = this._boundPaths[a];
          if (Path.isDescendant(a, path)) {
            this._notifyPath(Path.translate(a, b, path), value);
          } else if (Path.isDescendant(b, path)) {
            this._notifyPath(Path.translate(b, a, path), value);
          }
        }
      },

      _notifyPathUp: function(path, value) {
        var rootName = Path.root(path);
        var dashCaseName = Polymer.CaseMap.camelToDashCase(rootName);
        var eventName = dashCaseName + this._EVENT_CHANGED;
        // use a cached event here (_useCache: true) for efficiency
        this.fire(eventName, {
          path: path,
          value: value
        }, {bubbles: false, _useCache: Polymer.Settings.eventDataCache ||
          !Polymer.Settings.isIE});
      },

      _EVENT_CHANGED: '-changed',

      /**
       * Notify that an array has changed.
       *
       * Example:
       *
       *     this.items = [ {name: 'Jim'}, {name: 'Todd'}, {name: 'Bill'} ];
       *     ...
       *     this.items.splice(1, 1, {name: 'Sam'});
       *     this.items.push({name: 'Bob'});
       *     this.notifySplices('items', [
       *       { index: 1, removed: [{name: 'Todd'}], addedCount: 1, obect: this.items, type: 'splice' },
       *       { index: 3, removed: [], addedCount: 1, object: this.items, type: 'splice'}
       *     ]);
       *
       * @param {string} path Path that should be notified.
       * @param {Array} splices Array of splice records indicating ordered
       *   changes that occurred to the array. Each record should have the
       *   following fields:
       *    * index: index at which the change occurred
       *    * removed: array of items that were removed from this index
       *    * addedCount: number of new items added at this index
       *    * object: a reference to the array in question
       *    * type: the string literal 'splice'
       *
       *   Note that splice records _must_ be normalized such that they are
       *   reported in index order (raw results from `Object.observe` are not
       *   ordered and must be normalized/merged before notifying).
      */
      notifySplices: function(path, splices) {
        var info = {};
        var array = this._get(path, this, info);
        // Notify change to key-based path
        this._notifySplices(array, info.path, splices);
      },

      // Note: this implemetation only accepts key-based array paths
      _notifySplices: function(array, path, splices) {
        var change = {
          keySplices: Polymer.Collection.applySplices(array, splices),
          indexSplices: splices
        };
        var splicesPath = path + '.splices';
        this._notifyPath(splicesPath, change);
        this._notifyPath(path + '.length', array.length);
        // All path notification values are cached on `this.__data__`.
        // Null here to allow potentially large splice records to be GC'ed.
        this.__data__[splicesPath] = {keySplices: null, indexSplices: null};
      },

      _notifySplice: function(array, path, index, added, removed) {
        this._notifySplices(array, path, [{
          index: index,
          addedCount: added,
          removed: removed,
          object: array,
          type: 'splice'
        }]);
      },

      /**
       * Adds items onto the end of the array at the path specified.
       *
       * The arguments after `path` and return value match that of
       * `Array.prototype.push`.
       *
       * This method notifies other paths to the same array that a
       * splice occurred to the array.
       *
       * @method push
       * @param {String} path Path to array.
       * @param {...any} var_args Items to push onto array
       * @return {number} New length of the array.
       */
      push: function(path) {
        var info = {};
        var array = this._get(path, this, info);
        var args = Array.prototype.slice.call(arguments, 1);
        var len = array.length;
        var ret = array.push.apply(array, args);
        if (args.length) {
          this._notifySplice(array, info.path, len, args.length, []);
        }
        return ret;
      },

      /**
       * Removes an item from the end of array at the path specified.
       *
       * The arguments after `path` and return value match that of
       * `Array.prototype.pop`.
       *
       * This method notifies other paths to the same array that a
       * splice occurred to the array.
       *
       * @method pop
       * @param {String} path Path to array.
       * @return {any} Item that was removed.
       */
      pop: function(path) {
        var info = {};
        var array = this._get(path, this, info);
        var hadLength = Boolean(array.length);
        var args = Array.prototype.slice.call(arguments, 1);
        var ret = array.pop.apply(array, args);
        if (hadLength) {
          this._notifySplice(array, info.path, array.length, 0, [ret]);
        }
        return ret;
      },

      /**
       * Starting from the start index specified, removes 0 or more items
       * from the array and inserts 0 or more new itms in their place.
       *
       * The arguments after `path` and return value match that of
       * `Array.prototype.splice`.
       *
       * This method notifies other paths to the same array that a
       * splice occurred to the array.
       *
       * @method splice
       * @param {String} path Path to array.
       * @param {number} start Index from which to start removing/inserting.
       * @param {number} deleteCount Number of items to remove.
       * @param {...any} var_args Items to insert into array.
       * @return {Array} Array of removed items.
       */
      splice: function(path, start) {
        var info = {};
        var array = this._get(path, this, info);
        // Normalize fancy native splice handling of crazy start values
        if (start < 0) {
          start = array.length - Math.floor(-start);
        } else {
          start = Math.floor(start);
        }
        if (!start) {
          start = 0;
        }
        var args = Array.prototype.slice.call(arguments, 1);
        var ret = array.splice.apply(array, args);
        var addedCount = Math.max(args.length - 2, 0);
        if (addedCount || ret.length) {
          this._notifySplice(array, info.path, start, addedCount, ret);
        }
        return ret;
      },

      /**
       * Removes an item from the beginning of array at the path specified.
       *
       * The arguments after `path` and return value match that of
       * `Array.prototype.pop`.
       *
       * This method notifies other paths to the same array that a
       * splice occurred to the array.
       *
       * @method shift
       * @param {String} path Path to array.
       * @return {any} Item that was removed.
       */
      shift: function(path) {
        var info = {};
        var array = this._get(path, this, info);
        var hadLength = Boolean(array.length);
        var args = Array.prototype.slice.call(arguments, 1);
        var ret = array.shift.apply(array, args);
        if (hadLength) {
          this._notifySplice(array, info.path, 0, 0, [ret]);
        }
        return ret;
      },

      /**
       * Adds items onto the beginning of the array at the path specified.
       *
       * The arguments after `path` and return value match that of
       * `Array.prototype.push`.
       *
       * This method notifies other paths to the same array that a
       * splice occurred to the array.
       *
       * @method unshift
       * @param {String} path Path to array.
       * @param {...any} var_args Items to insert info array
       * @return {number} New length of the array.
       */
      unshift: function(path) {
        var info = {};
        var array = this._get(path, this, info);
        var args = Array.prototype.slice.call(arguments, 1);
        var ret = array.unshift.apply(array, args);
        if (args.length) {
          this._notifySplice(array, info.path, 0, args.length, []);
        }
        return ret;
      },

      // TODO(kschaaf): This is the path analogue to Polymer.Bind.prepareModel,
      // which provides API for path-based notification on elements with property
      // effects; this should be re-factored along with the Bind lib, either all on
      // Base or all in Bind (see issue https://github.com/Polymer/polymer/issues/2547).
      prepareModelNotifyPath: function(model) {
        this.mixin(model, {
          fire: Polymer.Base.fire,
          _getEvent: Polymer.Base._getEvent,
          __eventCache: Polymer.Base.__eventCache,
          notifyPath: Polymer.Base.notifyPath,
          _get: Polymer.Base._get,
          _EVENT_CHANGED: Polymer.Base._EVENT_CHANGED,
          _notifyPath: Polymer.Base._notifyPath,
          _notifyPathUp: Polymer.Base._notifyPathUp,
          _pathEffector: Polymer.Base._pathEffector,
          _annotationPathEffect: Polymer.Base._annotationPathEffect,
          _complexObserverPathEffect: Polymer.Base._complexObserverPathEffect,
          _annotatedComputationPathEffect: Polymer.Base._annotatedComputationPathEffect,
          _computePathEffect: Polymer.Base._computePathEffect,
          _notifyBoundPaths: Polymer.Base._notifyBoundPaths,
          _getPathParts: Polymer.Base._getPathParts
        });
      }

    });

  })();


</script>
